using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Xml;

namespace Td.Additional.ComponentModel
{
    /// <summary>
    /// Property set.
    /// See also:
    ///  - Microsoft: http://www.propertygridresourcelist.com
    /// => http://www.artiso.com/ProBlog/CategoryView,category,Know-How.aspx (!!!!!)
    ///  - http://www.codeproject.com/KB/cpp/dropdownproperties.aspx
    ///  - http://www.mycsharp.de/wbb2/thread.php?threadid=52766
    ///  - http://www.devgroup-stuttgart.de/Download/2004-02-25/The%20.NET%20PropertyGridThomas%20Becker.pdf
    ///    The property grid is very flexible. You must only do the following steps, 
    ///    to display and configure dynamic properties:
    ///    1. You need a class for every property, derived from PropertyDescriptor, 
    ///       where all needful must be implemented
    ///    2. Then you need a collection for these properties. This must be derived from 
    ///       ICollection, IEnumerable and ICustomTypeDescriptor. Also you should implement 
    ///       INotifyPropertyChanged to offer events, if a property would be changed.
    ///       Important is the method 'GetProperties' from the ICustomTypeDescriptor interface. 
    ///       This method is called by a property grid and returns our property collection.
    ///    3. For the internal property collection we need a PropertyDescriptorCollection 
    ///       or a derived from that class.
    /// </summary>
    [TypeConverter(typeof(PropertySetConverter))]
    public sealed class PropertySet : ICollection, IEnumerable, ICustomTypeDescriptor, INotifyPropertyChanged
    {
        #region Fields

        /// <summary>
        /// Sync root
        /// </summary>
        private object syncRoot;
        /// <summary>
        /// Property list
        /// </summary>
        private PropertyDescriptorCollection properties;
        /// <summary>
        /// Default property
        /// </summary>
        private PropertyDescriptor defaultProperty;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="PropertySet"/> class.
        /// </summary>
        public PropertySet()
        {
            properties = new PropertyDescriptorCollection(new Property[0]);
            defaultProperty = null;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Owner
        /// </summary>
        private PropertySet owner;

        /// <summary>
        /// Gets the owner.
        /// </summary>
        /// <value>The owner.</value>
        public PropertySet Owner
        {
            get { return owner; }
        }

        #endregion

        #region PropertySet members

        /// <summary>
        /// Gets the value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="caption">The caption.</param>
        /// <param name="key">The key.</param>
        /// <param name="preset">The preset.</param>
        /// <returns></returns>
        public T GetValue<T>(string caption, string key, object preset)
        {
            if (!Contains(caption, key))
                return (T)preset;
            return (T)this[caption, key];
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <param name="caption">The caption.</param>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public bool SetValue(string caption, string key, object value)
        {
            this[caption, key] = value;
            return true;
        }

        /// <summary>
        /// Removes the key.
        /// </summary>
        /// <param name="caption">The caption.</param>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public bool RemoveKey(string caption, string key)
        {
            if (!Contains(caption, key))
                return false;
            Remove(caption, key);
            return true;
        }

        /// <summary>
        /// Removes the caption.
        /// </summary>
        /// <param name="caption">The caption.</param>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public bool RemoveCaption(string caption, string key)
        {
            for (int i = properties.Count - 1; i >= 0; i--)
                if (properties[i].Category.Equals(caption, StringComparison.Ordinal))
                    RemoveAt(i);
            return true;
        }

        /// <summary>
        /// Gets the captions.
        /// </summary>
        /// <returns></returns>
        public List<string> GetCaptions()
        {
            List<string> result = new List<string>();
            for (int i = properties.Count - 1; i >= 0; i--)
                if (!result.Contains(properties[i].Category))
                    result.Add(properties[i].Category);
            return result;
        }

        /// <summary>
        /// Gets the caption keys.
        /// </summary>
        /// <param name="caption">The caption.</param>
        /// <returns></returns>
        public List<string> GetCaptionKeys(string caption)
        {
            List<string> result = new List<string>();
            for (int i = properties.Count - 1; i >= 0; i--)
                if (properties[i].Category.Equals(caption, StringComparison.Ordinal))
                    result.Add(properties[i].Name);
            return result;
        }

        /// <summary>
        /// Exports to XML.
        /// </summary>
        /// <returns></returns>
        public XmlDocument ExportToXml()
        {
            XmlDocument doc = new XmlDocument();
            doc.AppendChild(doc.CreateXmlDeclaration("1.0", null, null));
            doc.AppendChild(doc.CreateElement("propertyset"));
            foreach (string caption in GetCaptions())
            {
                XmlElement xmlCaption = doc.CreateElement("category");
                xmlCaption.SetAttribute("name", caption);
                foreach (string property in GetCaptionKeys(caption))
                {
                    Property propertyDescriptor = Find(caption, property);
                    XmlElement xmlProperty = doc.CreateElement("property");
                    xmlProperty.SetAttribute("name", property);
                    if (propertyDescriptor.PropertyType != null)
                        xmlProperty.SetAttribute("type", propertyDescriptor.PropertyType.ToString());
                    if (!string.IsNullOrEmpty(propertyDescriptor.Description))
                        xmlProperty.SetAttribute("description", propertyDescriptor.Description);
                    if (propertyDescriptor.DefaultValue != null)
                        xmlProperty.SetAttribute("defaultvalue", propertyDescriptor.DefaultValue.ToString());
                    if (propertyDescriptor.Value != null)
                        xmlProperty.InnerText = propertyDescriptor.Value.ToString();
                    xmlCaption.AppendChild(xmlProperty);
                }
                doc.DocumentElement.AppendChild(xmlCaption);
            }
            return doc;
        }

        /// <summary>
        /// Imports from XML.
        /// </summary>
        /// <param name="doc">The doc.</param>
        /// <returns></returns>
        public PropertySet ImportFromXml(XmlDocument doc)
        {
            if (doc == null)
                throw new ArgumentNullException("doc");
            PropertySet propertySet = new PropertySet();
            return null;
        }

        #endregion

        #region Adapted and appendixed IList Member

        /// <summary>
        /// Finds the specified name.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public Property Find(string name)
        {
            int index = name.IndexOf('/');
            if (index < 0)
                return Find(string.Empty, name);
            return Find(name.Substring(0, index), name.Substring(index + 1));
        }

        /// <summary>
        /// Finds the specified category.
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public Property Find(string category, string key)
        {
            foreach (Property property in properties)
                if (property.Category == category && property.Name == key)
                    return property;
            return null;
        }

        /// <summary>
        /// Adds the specified property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <returns></returns>
        public int Add(Property property)
        {
            if (property == null) return -1;
            property.owner = this;
            property.Changed = false;
            int result = properties.Add(property);
            if (result >= 0)
                OnPropertyChanged(this, new PropertyChangedEventArgs(property.Name));
            if (property.Value is PropertySet)
            {
                (property.Value as PropertySet).owner = this;
                (property.Value as PropertySet).PropertyChanged +=
                    delegate(object sender, PropertyChangedEventArgs e) { this.OnPropertyChanged(sender, e); };
            }
            return result;
        }

        /// <summary>
        /// Adds the specified category.
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <param name="propertyType">Type of the property.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public int Add(String category, String key, Type propertyType, object value)
        {
            return Add(new Property(category, key, propertyType, value));
        }

        /// <summary>
        /// Clears this instance.
        /// </summary>
        public void Clear()
        {
            properties.Clear();
        }

        /// <summary>
        /// Determines whether [contains] [the specified category].
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <returns>
        /// 	<c>true</c> if [contains] [the specified category]; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(String category, String key)
        {
            return Find(category, key) != null;
        }

        /// <summary>
        /// Indexes the of.
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        public int IndexOf(String category, String key)
        {
            Property property = Find(category, key);
            if (property == null) return -1;
            return properties.IndexOf(property);
        }

        /// <summary>
        /// Inserts the specified index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="property">The property.</param>
        public void Insert(int index, Property property)
        {
            if (property == null) return;
            properties.Insert(index, property);
            OnPropertyChanged(this, new PropertyChangedEventArgs(property.Name));
            if (property.Value is PropertySet)
            {
                (property.Value as PropertySet).owner = this;
                (property.Value as PropertySet).PropertyChanged +=
                    delegate(object sender, PropertyChangedEventArgs e) { this.OnPropertyChanged(sender, e); };
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is fixed size.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is fixed size; otherwise, <c>false</c>.
        /// </value>
        public bool IsFixedSize
        {
            get { return false; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is read only.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is read only; otherwise, <c>false</c>.
        /// </value>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Removes the specified category.
        /// </summary>
        /// <param name="category">The category.</param>
        /// <param name="key">The key.</param>
        public void Remove(String category, String key)
        {
            Property property = Find(category, key);
            if (property != null)
                properties.Remove(property);
        }

        /// <summary>
        /// Removes at.
        /// </summary>
        /// <param name="index">The index.</param>
        public void RemoveAt(int index)
        {
            properties.RemoveAt(index);
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object"/> at the specified index.
        /// </summary>
        /// <value></value>
        public object this[int index]
        {
            get { return properties[index].GetValue(this); }
            set { properties[index].SetValue(this, value); }
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object"/> with the specified key.
        /// </summary>
        /// <value></value>
        public object this[string key]
        {
            get {
                String category = "";
                int index = key.IndexOf("/");
                if (index >= 0)
                {
                    category = key.Substring(0, index);
                    key = key.Substring(index + 1);
                }
                Property property = Find(category, key);
                if (property != null)
                    return property.GetValue(this);
                return null;
            }
            set
            {
                String category = "";
                int index = key.IndexOf("/");
                if (index >= 0)
                {
                    category = key.Substring(0, index);
                    key = key.Substring(index + 1);
                }
                Property property = Find(category, key);
                if (property == null)
                    Add(category, key, value.GetType(), value);
                else
                    property.SetValue(this, value);
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object"/> with the specified category.
        /// </summary>
        /// <value></value>
        public object this[string category, string key]
        {
            get
            {
                Property property = Find(category, key);
                if (property == null)
                    return null;
                return property.GetValue(this);
            }
            set
            {
                Property property = Find(category, key);
                if (property == null)
                    Add(category, key, value.GetType(), value);
                else
                    property.SetValue(this, value);
            }
        }

        #endregion

        #region ICollection Member

        /// <summary>
        /// Copies the elements of the <see cref="T:System.Collections.ICollection"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
        /// </summary>
        /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.ICollection"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
        /// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
        /// <exception cref="T:System.ArgumentNullException">
        /// 	<paramref name="array"/> is null.
        /// </exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException">
        /// 	<paramref name="index"/> is less than zero.
        /// </exception>
        /// <exception cref="T:System.ArgumentException">
        /// 	<paramref name="array"/> is multidimensional.
        /// -or-
        /// <paramref name="index"/> is equal to or greater than the length of <paramref name="array"/>.
        /// -or-
        /// The number of elements in the source <see cref="T:System.Collections.ICollection"/> is greater than the available space from <paramref name="index"/> to the end of the destination <paramref name="array"/>.
        /// </exception>
        /// <exception cref="T:System.ArgumentException">
        /// The type of the source <see cref="T:System.Collections.ICollection"/> cannot be cast automatically to the type of the destination <paramref name="array"/>.
        /// </exception>
        public void CopyTo(Array array, int index)
        {
            properties.CopyTo(array, index);
        }

        /// <summary>
        /// Gets the number of elements contained in the <see cref="T:System.Collections.ICollection"/>.
        /// </summary>
        /// <value></value>
        /// <returns>
        /// The number of elements contained in the <see cref="T:System.Collections.ICollection"/>.
        /// </returns>
        public int Count
        {
            get { return properties.Count; }
        }

        /// <summary>
        /// Gets a value indicating whether access to the <see cref="T:System.Collections.ICollection"/> is synchronized (thread safe).
        /// </summary>
        /// <value></value>
        /// <returns>true if access to the <see cref="T:System.Collections.ICollection"/> is synchronized (thread safe); otherwise, false.
        /// </returns>
        public bool IsSynchronized
        {
            get { return false; }
        }

        /// <summary>
        /// Gets an object that can be used to synchronize access to the <see cref="T:System.Collections.ICollection"/>.
        /// </summary>
        /// <value></value>
        /// <returns>
        /// An object that can be used to synchronize access to the <see cref="T:System.Collections.ICollection"/>.
        /// </returns>
        public object SyncRoot
        {
            get
            {
                if (this.syncRoot == null)
                {
                    Interlocked.CompareExchange(ref this.syncRoot, new object(), null);
                }
                return this.syncRoot;
            }
        }

        #endregion

        #region IEnumerable Member

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator GetEnumerator()
        {
            return properties.GetEnumerator();
        }

        #endregion

        #region ICustomTypeDescriptor Member

        /// <summary>
        /// Returns a collection of custom attributes for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.AttributeCollection"/> containing the attributes for this object.
        /// </returns>
        public AttributeCollection GetAttributes()
        {
            return TypeDescriptor.GetAttributes(this, true);
        }

        /// <summary>
        /// Returns the class name of this instance of a component.
        /// </summary>
        /// <returns>
        /// The class name of the object, or null if the class does not have a name.
        /// </returns>
        public string GetClassName()
        {
            return TypeDescriptor.GetClassName(this, true);
        }

        /// <summary>
        /// Returns the name of this instance of a component.
        /// </summary>
        /// <returns>
        /// The name of the object, or null if the object does not have a name.
        /// </returns>
        public string GetComponentName()
        {
            return TypeDescriptor.GetComponentName(this, true);
        }

        /// <summary>
        /// Returns a type converter for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.TypeConverter"/> that is the converter for this object, or null if there is no <see cref="T:System.ComponentModel.TypeConverter"/> for this object.
        /// </returns>
        public TypeConverter GetConverter()
        {
            return TypeDescriptor.GetConverter(this, true);
        }

        /// <summary>
        /// Returns the default event for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptor"/> that represents the default event for this object, or null if this object does not have events.
        /// </returns>
        public EventDescriptor GetDefaultEvent()
        {
            return TypeDescriptor.GetDefaultEvent(this, true);
        }

        /// <summary>
        /// Returns the default property for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptor"/> that represents the default property for this object, or null if this object does not have properties.
        /// </returns>
        public PropertyDescriptor GetDefaultProperty()
        {
            return defaultProperty;
        }

        /// <summary>
        /// Returns an editor of the specified type for this instance of a component.
        /// </summary>
        /// <param name="editorBaseType">A <see cref="T:System.Type"/> that represents the editor for this object.</param>
        /// <returns>
        /// An <see cref="T:System.Object"/> of the specified type that is the editor for this object, or null if the editor cannot be found.
        /// </returns>
        public object GetEditor(Type editorBaseType)
        {
            return TypeDescriptor.GetEditor(this, editorBaseType, true);
        }

        /// <summary>
        /// Returns the events for this instance of a component using the specified attribute array as a filter.
        /// </summary>
        /// <param name="attributes">An array of type <see cref="T:System.Attribute"/> that is used as a filter.</param>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptorCollection"/> that represents the filtered events for this component instance.
        /// </returns>
        public EventDescriptorCollection GetEvents(Attribute[] attributes)
        {
            return TypeDescriptor.GetEvents(this, attributes, true);
        }

        /// <summary>
        /// Returns the events for this instance of a component.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.ComponentModel.EventDescriptorCollection"/> that represents the events for this component instance.
        /// </returns>
        public EventDescriptorCollection GetEvents()
        {
            return TypeDescriptor.GetEvents(this, true);
        }

        /// <summary>
        /// Returns the properties for this instance of a component using the attribute array as a filter.
        /// </summary>
        /// <param name="attributes">An array of type <see cref="T:System.Attribute"/> that is used as a filter.</param>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptorCollection"/> that represents the filtered properties for this component instance.
        /// </returns>
        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return GetProperties();
        }

        /// <summary>
        /// Returns the properties for this instance of a component.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.ComponentModel.PropertyDescriptorCollection"/> that represents the properties for this component instance.
        /// </returns>
        public PropertyDescriptorCollection GetProperties()
        {
            return properties;
        }

        /// <summary>
        /// Returns an object that contains the property described by the specified property descriptor.
        /// </summary>
        /// <param name="pd">A <see cref="T:System.ComponentModel.PropertyDescriptor"/> that represents the property whose owner is to be found.</param>
        /// <returns>
        /// An <see cref="T:System.Object"/> that represents the owner of the specified property.
        /// </returns>
        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return this;
        }

        #endregion

        #region INotifyPropertyChanged Member

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Called when [property changed].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        internal void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler tmpHandler = PropertyChanged;
            if (tmpHandler != null)
                tmpHandler(sender ?? this, e);
        }

        #endregion

        #region Overriden object members

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            String result = String.Empty;
            foreach (Property property in GetProperties())
            {
                if (result != "") result += "; ";
                result += property.ToString();
            }
            return result;
        }

        #endregion
    }
}